「たのしいコーディングのための"CUPID"特性」
この文書はDan Northによる "CUPID—for joyful coding" の全文訳です 訳文へのフィードバックは訳者までお寄せください(twitterやDiscordなど) 翻訳前の原著作者は「Dan North & Associates Ltd」です
このページの内容はこのクリエイティブ・コモンズライセンスの範囲内において自由に利用できます
hr.icon
たのしいコーディングのための「CUPID」特性
当初はちょっとしたSOLID批判のつもりが、「藪を突ついて蛇を出して」しまったのですが、物事はそこから具体的で目に見えるものへと発展しました。仮に、近頃はSOLID原則が役に立たなくなっているのだとしたら、何に置き換えればよいのでしょう? あらゆるソフトウェアに通用する原則はあるのでしょうか? そもそも「原則」とは何を意味するのでしょう? 私は「仕事がたのしくなるソフトウェアならではの特性や性質がある」ということを確信しています。コードでそのような質が高まれば高まるほど、仕事もどんどんたのしくなります。しかし、何事もトレードオフですから、自分の置かれている状況をつねに考慮する必要があります。
そうした特性はたくさん存在しており、互いに重なりや関連がありますし、説明の仕方もさまざまです。ここでは私がコードで気にかけている要素を強く支えていると思える5つを選びました。選ぶ数はこれぐらいが丁度良いでしょう。便利な頭字語も作れますし、覚えやすいからです。
個別の特性については今後の記事で詳しく説明していく予定です。今回はこの記事をこれ以上長くしないために、あまり包括的でありませんが、その点についてはご容赦ください。
Composable(組み立て可能): 他とうまくやれる
Unix哲学:ひとつのことをうまくやる
Predictable(予測可能): 期待どおりに動作する
Idiomatic(慣習に従っている): 自然に感じられる
Domain-based(ドメイン準拠): 解決領域は、言葉と構造で問題領域をモデル化する
hr.icon
はじめに: 昔むかし…
よく知らないコードベースなのに、どうすればいいのかが「わかってしまった」ことはありますか? コードの構造も名前も処理の流れも明かで、見覚えがある気すらします。笑顔が浮びます。「なるほどね!」といった具合です。
幸運にも私自身は30年にわたるキャリアのなかで、何度もこうした経験があり、毎度「たのしさ」に満たされてきました。そうした体験に初めて遭遇したのは1990年代初頭のことで、いまでも鮮烈に覚えています。それは、デジタル印刷向けに複雑な画像処理をするC言語の巨大なコードベースの解読を試みていたときのことでした。「他の誰かのコード™」にバグがあったので、原因を突き止めて修正することになったのです。駆け出しプログラマーだった当時の自分の感覚をよく覚えています。それは気の重さと、ずぶの素人だという自分の本性があらわになることへの恐怖が入り混じったものでした。
私のエディタ(viとctagsです)は関数の呼び出し元から関数定義に移動することができました。数分もすると、私は数百ものソースとヘッダファイルからなるコードベースの、入り組んだ呼び出しの奥深くに辿りついたのですが、そこで私は「自分が何を探しているのか、完全にわかっている」と感じていました。バグの原因はすぐに突き止められました。単純なロジックの誤りだったので、修正してコードをビルドして、テストしました。といっても使うのはMakefileだけで、自動化されたテストなどありません。TDDの登場はそこから10年後の話で、当時のC言語にはそうしたテストのツールは存在していませんでした。
いくつかサンプル画像を変換してみたところ、問題なさそうでした。私は自分がやり遂げたことに自信を持てました。それは、a) バグを発見して修正できたことと、b) 同時に別の厄介事を持ち込まなかったことです。
hr.icon
たのしいソフトウェア
作業するのがたのしいコードというものがあります。どこに何があって、何をすればいいのかを把握しています。どうやって変更をすればいいのかもわかっています。目当てのコードの場所には容易に辿りつけますし、動作は簡単に理解しやすく、推しはかることもできます。自分の加えた変更は期待どおりの効果をもたらして、不当な副作用は引き起こさないだろうと確信できます。コードが あなたを導き、周りをよく見るように促します。あなたの前にここへやって来たプログラマー達は、後から来る人たちのことを気にかけていました。ひょっとすると彼らは「後から来る人たち」が自分たち自身かもしれないことを承知していたのかもしれません!
「コンパイラがわかるコードは誰にでも書ける。すぐれたプログラマは人間にとってわかりやすいコードを書く<訳注1>」—『リファクタリング』Martin Fowler, Kent Beck, 1996 2000年代初頭に本書を読んだ私は、彼の言葉にそれまでのプログラミング観がひっくり返されました。「良いプログラミング」が、コードを「他の人間にとって」わかりやすくすることだとしたら? その「人間」のひとりが未来の私自身なら? それは、何だか私が目指すべきもののように思えました。
しかし「わかりやすいコード」は立派な目標だとは思うものの、難攻不落というほどではありません! Martinが『リファクタリング』を出版したのと同じ頃に、コンピューティングのパイオニアであるRichard P. Gabriel はコードが「居住可能である(habitable)」というアイデアについて、次のように述べました。 「居住可能性(Habitablility)とはソースコードの特徴であり、(人間にとって)その構造や意図が把握しやすくなっており、心地よくかつ自信を持って変更を加えられるものである。」
「居住可能性はその場所を住みやすくする。自宅のように。」
こちらのほうが追い求めるべきものに思えました。「心地よくかつ自信を持って」他人の書いたコードを変更できたらどんなに素敵だろう? コードを「住みやすくする」ことができるなら、「たのしくする」はどうだろう? 「たのしい」コードベースというものは可能だろうか?
もしプログラミングを仕事にしているなら、コードベースを行き来して操作することがあなたのユーザー体験を形づくります。驚き、苛立ち、恐怖、期待、無力感、希望、たのしさ、そのすべては、そのコードベースで以前に作業したプログラマーの選択によってもたらされるのです。
仮にコードベースを「たのしい」ものにできるとした場合、それぞれのコードベースがあなたの精神に与える影響は固有のもので、ふたつとない雪の結晶なのでしょうか? それとも、何が「たのしい」感情につながるのかを明らかにすることは可能で、それを強めるための道筋を示すことができるのでしょうか?
原則よりも特性
SOLIDの5つの原則についての返答をまとめだした当初は、それぞれの原則をもっと有用で関連性の高い原則に置き換えることを想定していました。しかし、すぐに「原則」というアイデアそのものに問題があることに気づきました。というのも、原則には「従うか、従わないか」しかありません。このことは、人々が共有する価値に集まる「中心集合(centred sets)」ではなく「ルールに従う者とルールに従わせる者」という「有界集合(bounded sets)」を生み出すことになります<原注2>。
私は「原則」よりも「特性」、すなわち「従うべきルール」よりも「コードの質や特徴」について考えるようになりました。特性は向うべきゴールや中心(centre)を定めます。あなたの書くコードは中心に近づいたり遠ざかったりしますが、そこには明確に方向があります。特性はコードを評価するレンズやフィルターとして使えるので、次に何を解決すべきかの判断に役立ちます。 CUPIDの特性はいずれも互いに関連しているので、ひとつの特性を改善すると、他の特性にも好影響をおよぼします。
特性の特性
では、そうした特性はどのようにして選べばよいでしょうか? 何がある特性の有用性の優劣を決めるのでしょうか? 私はCUPIDのそれぞれの特性には3つの「特性の特性」が備わっていてほしいと考えました。その3つとは 実用的(practical)、人間的(human)、多層的(layered)です。
特性が「実用的である」には次の要素を満たす必要があります。
明確化(articulate)しやすい。その特性は数センテンスで説明可能で、具体的な事例と反例を示すことができます
評価(assess)しやすい。その特性はコードのレビューや議論のレンズとして使えるので、コードにその特性がどの程度備わっているのかを容易に判断できます
適応(adopt)しやすい。CUPIDのいずれの特性も、少しずつインクリメンタルに取り入れてコードを発展させていくことができます。「全部入り」もなければ「失敗」もありません。決して「完了」することはなく、コードにはつねに改善の余地があります
特性が「人間的である」とは、コードではなく「人間」の観点から解釈されるということです。CUPIDが扱うのは「コードで作業したときに味わう気分」であって「コード自体の抽象的な説明」ではありません。たとえば、「ひとつのことをうまくやる」というUnix哲学は単一責任の原則と同じに見えますが、前者の対象が「コードの使い方」であるのに対し、後者のそれは「コードの内部」です<原注3>。 特性が「多層的である」とは、初学者にとってはガイダンスを(これは「明確化しやすい」ことの帰結です)、ソフトウェアの本性をもっと深く追究したい熟練者にはニュアンスを提供するということです。CUPIDのそれぞれの特性は、その名前と概要だけで「明白な」ものですが、そこにはさまざまな階層や次元、取り組み方があらわれています。各特性の「中心」は説明できるかもしれませんが、だとしてもそこに至る道筋はさまざまなのです!
hr.icon
Composable: 組み立て可能
使いやすいソフトウェアは、何度も何度も使われます。そうしたソフトウェアには、多かれ少なかれ「コードを組み立てやすい」という特性が備わっています。とはいえそれは必要条件でもなければ十分条件でもありません。どの場合でも、それぞれの観点からの反例を挙げることができます。よって、この特性は有用なヒューリスティクスと見なすのがよいでしょう。何事も多ければ多いほど良いというものではなく、すべてはトレードオフなのです。
表面積が小さい
扱う範囲の絞られた・主張の強いAPIは、学ぶべきことも間違えることも少なく、他のコードと合わせて使っても衝突したり一貫性を損なう可能性が少なくなります。その一方で、利便性の減少ももたらします。APIの扱う範囲が狭すぎると、複数のAPIをまとめて使うことになります。これは一般的なユースケースであっても「正しい組み立て方」を把握することが暗黙の了解となるので、使いはじめる障壁になるかもしれません。APIの粒度を適切に定めるのは、見かけよりもずっと難しいものです。断片化と肥大化の間のどこかに「ちょうどいい」凝集性のスイートスポットが存在します。
意図をあらわす
「意図をあらわすコード(Intention-revealing code)」は見つけるのも評価するのも簡単です。コンポーネントは簡単に見つけ出せますし、それが自分にとって必要なのどうかも素早く判断できます。私が気に入っている方式は(由緒あるオープンソースプロジェクトであるXStreamのように)、まず2分版のチュートリアルと10分版のチュートリアルが用意されていて、それから詳細を説明するやり方です。これだと少しずつ使い方を学びながら、自分に合わないと判明した時点で他の選択肢に切り換えられます。 私は、意図をあらわす名前を持ったクラスを定義しようとしたタイミングで、IDEからポップアップ表示で同じ名前のクラスをインポート候補として提案されたことが何度もあります。他の誰かが既に同じことを考えていて、私も似たような名前を選んだことで、思いがけずそのコードに行き当たったというわけです。これは単なる偶然ではありません。私たちが同じドメインに精通していたことで、似たような名前を選ぶ可能性が高まったのですから。コードが「ドメイン準拠」になっていると、その可能性はさらに高くなります。
依存が最小限
依存が最小限になっているコードは心配事を減らしますし、バージョンやライブラリの非互換が発生する確率も下がります。私の初めてのオープンソースプロジェクトは、Javaを使ったXJBなのですが、ロギングフレームワークにはほとんどどこでも使われているlog4jを採用していました。とある同僚から、このプロジェクトはライブラリとしてlog4jに依存しているだけでなく、その特定バージョンに依存しているとの指摘を受けました。そんなことは思いもよりませんでした。どうしてロギングのように無難なはずのライブラリのことを心配しなければならないのでしょう。そこで、私たちはこの依存を取り除いて、後にはJavaの動的プロキシを使って面白いことをする独立したプロジェクトとして括り出しました。このプロジェクト自体も依存は最小限になっています。 hr.icon
Unix哲学
私はUnixと同い歳で、どちらも1969年に生まれました。いまやUnixは地球上で最も普及しているOSです。1990年代に主要なオープンソースのOSであるLinuxとFreeBSDが普及するまでは、あらゆるコンピューターハードウェアメーカーはそれぞれ独自のUnixを開発していました。こんにちではほとんどすべての業務用サーバーは、クラウドであれオンプレミスであれ、Linux形式になりました。組み込みシステムやネットワーク機器でも稼動していますし、macOSやAndroid OSを支えています。さらにはMicrosoft Windowsのオプションのサブシステムにもなっているのです!
シンプルで一貫したモデル
では、通信研究所で生まれ、大学生が趣味のプロジェクトとしてコピーしたニッチなオペレーティングシステムが、どのようにして世界最大のオペレーティングシステムになったのでしょうか? 当時のOSベンダーがその技術力で名を馳せたのと同じぐらい、訴訟を起こすことでも有名だったという商業的・法的な時代背景があったことは間違いありませんが、その色褪せない技術的な魅力はシンプルで一貫した設計思想にあります。 Unix哲学では、うまく連携できるコンポーネントを書くためには(「組み立て可能」の特性で述べたように)「ひとつのことをうまくやる」ようにと言われています<原注4>。たとえば、lsコマンドは、ファイルやディレクトリの詳細をリストアップしますが、ファイルやディレクトリについては何も把握していません! 実際にはstatというシステムコマンドが情報を提供して、lsは単に受け取った情報を文字列として表示しているだけなのです。 他にもたとえば、catコマンドは1つ以上のファイルの中身を出力(連結)し、grepは与えられたテキストから与えられたパターンにマッチするテキストを選択し、sedはテキストのパターンを置き換える、といった具合です。Unixコマンドラインには「パイプ」という強力な概念があります。これにより、あるコマンドの出力を次のコマンドの入力として接続することで、選択・変換・フィルタリング・ソートといった処理のパイプラインを構築できます。それぞれが「ひとつのことをうまくやる」ように見事に設計されたコマンドを複数活用して組み立てることで、高度なテキストやデータ処理プログラムを書くことができるのです。
単一目的 vs. 単一責任
この特性は一見すると、SRP(Single Responsibility Principle: 単一責任の原則)と同じに思えます。実際のところ、SRPの言わんとするところと重なる点もあります。しかし「ひとつのことをうまくやる」は、「外から内(outside-in)」の視点です。これは具体的で、うまく定義された、包括的な目的を備えた特性といえます。SRPは「内から外(inside-out)」の視点であり、コードの編成の話です。 SRPとは、この用語を提唱したRobert C. Martinによると(コードを)「変更する理由は、ひとつだけであるべきである」という原則です。Wikipediaの記事では報告書を出力するモジュールを例として、 報告書の内容と形式はそれぞれ独立した関心事なので、別々のクラスやモジュールに分けるように考えるべきだとしています。個人的な経験(別の記事で言及しました)では、これは作為的な接ぎ目(artificial seam)を生み出します。よくあるケースとして、データの変更にともなって内容も形式も一緒に変更されることがあります。たとえば、新しくフィールドが追加されたり、データの取得元が変更されると、出力させたい内容と出力したい形式の両方に影響がおよぶことがあります。 他によくあるシナリオは「UIコンポーネント」です。SRPではコンポーネントのレンダリングとビジネスロジックを分離させることを必須としています。両者を別々の場所に配置すると、開発者には「同一フィールド間を連携させる」という管理の手間が発生します。より大きなリスクは、これが「早まった最適化」かもしれないことです。コンポーネントが「ひとつのことをうまくやる」ことができるようになったり、問題空間のドメインに準拠していったりといった、コードベースの成長によってあらわれる、より自然な関心事に基づく分離を妨げてしまうかもしれません。コードベースは、成長するといずれ適切なサブコンポーネントに分割すべき時期に至りますが、そうした構造を「いつ」「どのように」変えていくべきかの指標としては、「組み合せ可能である」や「ドメイン準拠」といった特性のほうが適切になるでしょう。
hr.icon
Predictable: 予測可能
コードは、見た目どおりに、一貫して、確実に、(悪い意味での)驚きなしに動くべきです。コードはその確認が単に可能であるだけでなく、容易におこなえる必要があります。その意味で、予測可能性はテスト可能性の一般化であるといえます。
予測可能なコードは期待どおりに振る舞うべきですし、決定論的かつ観測可能であるべきです。
期待どおりに振る舞う
「システムは本番稼動すると、ある意味、それが仕様となります」— Michael Feathers
つまり「すべてのテストにパスする」ことは必須ではありませんし、テスト駆動開発を道具というよりも宗教のように捉えている人がいることということもわかってきました。過去に私は複雑なアルゴリズム取引アプリケーションの仕事をしたことがあるのですが、そこでの「テストカバレッッジ」は7%程度でした。しかもテストは均等分布ではありませんでした! ほとんどのコードに自動化されたテストはまったく存在しておらず、一部のコードに途方もない量の高度なテストが大量に用意されていて、そこでは微妙なバグやエッジケースが検証されていました。それでも当時はコードベースの大部分に自信をもって手を入れることができました。なぜなら、それぞれのコンポーネントは「ひとつのことをうまくやる」ようになっていて、振る舞いは素直で予測可能だったので、どこを変更すべきかはいつも明白だったからです。
決定論的である
ソフトウェアは何度実行しても同じ挙動であるべきです。たとえば、乱数生成器や動的計算のような非決定論的に設計されたコードであっても、運用面や機能面での境界は定義できます。メモリやネットワーク、ストレージ、処理境界(processing boundaries)や時間境界(time boundaries)、その他の依存要素を想定できたほうがよいでしょう。
決定論は幅の広いトピックです。予測可能性の観点からは、決定論的なコードとは「堅牢性と信頼性を備えたレジリエントなコード」のことです。
堅牢性とは、守備範囲の広さや完全さのことです。限界やエッジケースが明白であるべきです。
信頼性とは、守備範囲において期待どおりに動作するということです。実行結果は毎回同じになるべきです。
レジリエンスとは、守備範囲にない状況、すなわち入力や動作環境における予期せぬ事態にどれだけうまく対処できるかということです。
観測可能である
制御理論の観点からも、コードは観測可能であるべきです。コードはその出力から内部を推測できますが、それが可能になるのは、そうなるように設計した場合に限ります。複数コンポーネント間での連携が生じると(特にそれが非同期である場合)、予測していなかった振る舞いが生じて、結果が非線形になってしまいます。 当初からコードを測定可能にしておくと、実行時の特性を理解する手がかりとなる貴重なデータが得られるようにります。以下に4段階のモデルを示します。おまけも2つつけておきましょう!
1. インストゥルメンテーション(Instrumentation) によって、ソフトウェアが何をしているのかを伝えます
2. テレメトリ によって、情報を入手可能にします。プル型(要求する)であれプッシュ型(送出する)であれ「離れた場所から計測」できます
3. モニタリング によって、インストゥルメンテーションの内容を可視化します
4. アラート によって、モニターしているデータや、データのパターンに反応します
おまけ:
5. 予測(Predicting) 。データを活用して事象の発生を事前に予期します
6. 適応(Adapting) 。予測された事象の発生を先取りしたり、発生から回復するために、システムを動的に変更します
ほとんどのソフトウェアが最初の段階すら踏んでいません。稼働中のシステムを検査したり、手を加えることでインサイトの程度を高めるツールも存在していますが、アプリケーションの設計としてきちんと組み込まれたインストゥルメンテーションには到底かないません。
hr.icon
Idiomatic: 慣習に従っている
誰にだって自分好みのコーディングスタイルがあるものです。インデントはスペースかタブか、変数の命名、ブレースやカッコの置き方、ソースファイルのレイアウトなど、枚挙にいとまがありません。それに加えて、別のレイヤーでの好みもあるでしょう。ライブラリ、ツールチェーン、デプロイまでのワークフロー、バージョン管理のコメントスタイルやコミットの粒度などです(バージョン管理はしてますよね?)。
こうした好みは、不慣れなコードで作業する際に、余分な課題外在性の認知負荷をもたらすことになります。問題領域と解決空間を理解することに加えて、他の誰かが書いたコードの意図も解釈しなければなりません。すなわち、それが状況に即して考え抜かれた末の判断なのか、それとも恣意的な習慣なのかを判別する必要があるのです。
プログラミングのすぐれた特性は「共感」です。ユーザーへの共感、サポートしてくれる人たちへの共感、将来の開発者たち(それは未来の自分かもしれません)への共感。「人間にとってわかりやすいコードを書く」というのは他の誰かのためにコードを書くということです。これがコードが「習慣に従っている」ことの意味です。
その意味で、あなたが想定すべき相手とは:
その言語やライブラリ、ツールチェーン、エコシステムに馴れ親しんでいる、
ソフトウェア開発のことを理解できる経験を積んだプログラマーで、
仕事を終わらせようとしている
…ような人です!
言語のイディオム
コードはその言語のイディオムに沿うべきです。コードの見た目に強いこだわりがある言語では、自分の書いたコードがイディオムに沿っているかも簡単に評価できます。さほどこだわりが強くない言語では、「スタイルを選択」して、それに従う責任は書き手に委ねられます。PythonやGoはこだわりの強い言語の例です。
Pythonプログラマーはコードのイディオムを表現するために「Pythonらしい(Pythonic)」という用語を使います。Pythonには素敵なイースターエッグがあり、REPL上でimport thisするか、シェルからpython -m thisを実行すると、「The Zen of Python」と題されたプログラミングの格言が表示されます。ここではイディオムに従ったコードの真髄が語られています。たとえば「物事のやり方は、明白なのが一つ、できればたった一つだけであるべきだ」といったようにです。
Goではgofmtというソースコードの見た目を統一するコードフォーマッターが標準添付になっています。これにより、インデントやカッコの位置などの構文上の癖が一気に解消されます。おかげで、ライブラリのドキュメントやチュートリアルで触れるサンプルコードの見た目は一貫したものになっています。Goには、言語仕様に留まらず、Goのイディオムを紹介する「Effective Go」のようなドキュメントもあります。 いま挙げた言語らとは正反対に位置するのが、Scala、Ruby<原注5>、JavaScript、そして、それについては深い伝統を持つPerlです。これらの言語はあえてマルチパラダイムを採用しています。Perlでは「物事にはいろんなやり方がある(There Is More Than One Way to Do It)」の頭字語である「TIMTOWDI」という標語が提唱されました(読み方は「Tim Toady」です)。こうした言語では関数型でも手続き型でもオブジェクト指向でもコードを書くことができるので、自分の知っている他の言語から移行するのに必要な学習曲線を緩やかにします。
たとえば、連続した値を単純に処理するような場合、ほとんどの言語で次のように書けると思います。
イテレータを使う
インデックス付きforループを使う
条件付きwhileループを使う
関数パイプラインとコレクターを使う(いわゆるMap-Reduce)
末尾再帰関数を書く
単純とはいえない規模のコードには、ここで挙げた例のどれもが含まれているでしょうし、なかにはそれらが組み合わさったものもあるはずです。繰り返しになりますが、これはいずれも認知負荷を高めます。直面する問題について考える余裕に悪影響を与え、不確実性を増大させ、たのしさを減少させます。
コードのイディオムはあらゆるレベルの粒度に存在します。関数や型、パラメーター、モジュールの命名、コードのレイアウト、モジュールの構造、ツールの選択、依存関係の選択とその管理方法など、多岐にわたります。
採用しているテクノロジースタックの「こだわり度合(opinionatedness)」がどの程度であれ、その言語のイディオムやエコシステム、コミュニティで好まれているスタイルを時間をかけて学ぶことで、あなたの書くコードはもっと共感をもたらす、たのしいものになるはずです。
あるテクノロジーについて、学習曲線の現在の時点にあなたがいる期間は、あなたの書いたコードが存続する時間より短いはずです。よって、「現時点のあなたにとって読みやすいコード」を書きたい誘惑に抗うことは重要です。なぜなら「その人物」は長くは存在しないからです! 書いているコードがイディオムに従っていると自信を持てるようになるには、時間をかけてイディオムを学ぶしかありません。
独自のイディオム
言語に定まったイディオムがなかったり、複数の選択肢がある場合、「良い」スタイルがどのようなもので、一貫性を促すための制約やガイドラインの導入をどうするかは、あなたやチームの意向次第です。こうした制約は、IDEで使う共通のフォーマットルールや、コードのリントや検査をおこなう「ビルド警察」的なツール、標準で採用するツールチェーンに関する合意など、単純なものでかまいません。
hr.icon
Domain-based: ドメイン準拠
私たちはニーズに応えるべくソフトウェアを書きます。求められる内容は個別の状況を満たすための場合もあれば、より一般的で広範に及ぶ場合もあります。目的が何であるにせよ、「書いたコード」と「そのコードがすること」との間にある「認知距離(cognitive distance)」を最短にするには、コードは自身が何をしているかを問題領域の言葉で伝える必要があります。これは単に「正しい言葉を使う」こと以上に重要です。
ドメイン準拠の言葉
プログラミング言語やライブラリは、コンピュータサイエンスな要素に満ちています。ハッシュマップ、連結リスト、ツリーセット、データベースコネクション、などなど。基本データ型は、整数、文字、ブーリアンなどです。人物の姓をstring[30]として宣言してもきちんとデータを保存できると思いますが、Surname型を定義したほうが「意図をあらわす」コードになるでしょう。姓に関連する操作やプロパティ、制約を持たせることもできます。銀行系ソフトウェアの微妙なバグの多くは、金額を浮動小数点数で表現していることに起因します。練度の高い金融系プログラマーであれば、Money型を定義して、CurrencyとAmountを備えた複合型にするでしょう。
型や操作にうまく名前をつけることは、バグの発見や防止につながるだけでなく、コードで解決空間を明瞭に表現し、案内することを簡単にします。これを私は「ドメインの言葉を使ったコード」と題した記事として『プログラマが知るべき97のこと』』に寄稿しました。 「傍から見るとコードとドメインのどちらを議論しているのかわからない」というのは、ドメイン駆動のコードの成功を判定するひとつの基準になります。とある電子取引システムの仕事で私はそれを経験しました。あるとき、金融アナリストが複雑な取引価格設定のロジックについて2人のプログラマーと話し合っていました。私は彼らが価格設定のルールについて議論していると思っていたのですが、実は画面いっぱいに表示されたコードを指差しながら価格設定のアルゴリズムの話をしており、コードの1行1行の読み方を話題にしていたのです! ここでは問題領域と解決空間のコードとの認知距離は構文上の記号だけだったというわけです!
ドメイン準拠の構造
ドメイン準拠の言葉づかいは重要ですが、コードの構造も同じぐらい重要です。多くのフレームワークが手早く始められるようにスタブファイルをディレクトリに配置する「スケルトンプロジェクト」を提供しています。しかしこれは、解決しようとしている問題とは何ら関係のない構造をあらかじめコードベースに押しつけることになります。
そうではなく、コードの構成(ディレクトリ名とサブフォルダや隣接フォルダの編成、関連するファイルのグルーピングと命名)は、問題領域をできる限り反映させたものにすべきです。
アプリケーションフレームワークのRuby on Railsは2000年代初頭に、この方式をツール自体に組み込むことで普及しました。Railsが広く受け入れられたので、後続のフレームワークもこぞってこのアイデアを取り入れました。CUPIDは特定の言語やフレームワークを前提にしていませんが、Railsは 「ドメイン準拠」と「フレームワーク準拠」の構造の違いを理解する上で有用な事例なので、ここで取り上げます。 以下は、Railsアプリケーションが生成するスケルトンのディレクトリ構成です。Railsアプリケーションでは、開発者はほとんどの時間をappディレクトリで過ごします。執筆時点でスケルトン全体には、およそ60のファイルと50のディレクトリが存在します<原注7>。
code:_
app
├── assets
│ ├── config
│ ├── images
│ └── stylesheets
├── channels
│ └── application_cable
├── controllers
│ └── concerns
├── helpers
├── javascript
│ └── controllers
├── jobs
├── mailers
├── models
│ └── concerns
└── views
└── layouts
例として病院管理のアプリケーションがあるとしましょう。この病院には診療記録管理部門があります。その場合、少なくとも次のようなものが必要になると考えられます。
どこかしらのデータベースに対応づけられるモデル
画面に診療記録を表示するためのビュー
ビューとモデルの間を取り持つコントローラー
これに加えてヘルパーやアセット、それ以外にもフレームワーク固有の要素であるconcerns、mailers、jobs、channelsなどを扱う場合もあるでしょう。Rubyのコントローラーと共存するためのJavaScriptのコントローラーも必要になるかもしれません。いずれも意味的には密接に結合しているにもかかわらず、それぞれ別のディレクトリに格納されます。
この場合、診療記録管理にとって意味のある変更は、コードベース全体に波及する可能性が高くなります。SOLID原則のひとつであるSRPによれば、ビューのコードとコントローラーのコードは分離されているべきです。Railsのようなフレームワークでは、これを「両者をまったく別の場所に配置する」と解釈しています。これは認知負荷を高め、凝集性が低減し、プロダクトを変更する手間が余計にかかることになります。既に説明したとおり、教条的な制約は仕事をややこしくし、コードベースのたのしさを削ることになります。
コードをどのように配置するにせよ、モデルやビュー、コントローラーのような作成物は必要です。しかし、これらを種類ごとでグループ化するような構造を優先すべきではありません。そうではなく、コードベースのトップレベルでは病院管理の主要なユースケースを示すのがよいでしょう。例えば、patient_history、appoinments、staffing、それからcomplianceといったようにです。
コードの構造にドメイン準拠のアプローチを採用すると、何のためのコードなのかを把握しやすくなります。そうなっていれば、「ボタンの色を水色にする」以上に込み入ったことをする場合に、必要な場所へ簡単に辿りつけるようになります。
ドメイン準拠の境界
思いどおりのコードの構造と、思いどおりのコードの名前づけを果たせれば、モジュールの境界がドメインの境界となり、素直にデプロイできるようになります。ひとつのコンポーネントとしてデプロイしたい生成物を一緒に揃えられると、ドメイン境界がデプロイ境界に沿ったものになるので、凝集したビジネスコンポーネントやサービスとしてデプロイできます。プロダクトやサービスの構成が単一のモノリスであれ数多くのマイクロサービスであれ、あるいは両者の中間のどこかに位置にあろうとも、こうした調整をおこなうことでデプロイまでのワークフローの複雑性を低減できます。何かを見落したり、異なる環境や別のサブシステムの生成物を含めるような状況を減らすことができるのです。
これが適用されるのは、単一の、フラットな、トップレベルのコード構造に限りません。ドメインにはサブドメインが含まれます。コンポーネントにはサブコンポーネントが含まれます。デプロイは、抱えているリスク状況と変更内容に即して意義のある単位であれば、どの粒度でもおこなわれます。コード境界をドメイン境界に沿って調整することで、こうした選択肢を検討しやすくなりますし、管理も容易になります。
hr.icon
おわりに
組み立て可能、Unix哲学、予測可能、慣習に従っている、ドメイン準拠といった特性が備わっているコードは、そうでないコードよりも心地よく作業できることを私は確信しています。それぞれの特性は単独でも価値はありますが、相互に補完し合う存在です。
組み立て可能になっている(ひとつのことをうまくやる)コードは信頼のおける友人のような存在です。慣習に従っているコードとは初対面であっても親しみをおぼえます。予測可能なコードは他所でびっくりするための余裕を与えてくれますし、ドメイン準拠のコードはニーズと解決策の間の認知距離を縮めてくれます。コードをこうした特性の「中心」に向かわせていくと、当初よりも良くなっていることがわかります。
CUPIDはバクロニムとして考案したので、頭文字それぞれには複数の候補がありました。私がこの5つの特性を選んだのは、これらが「基礎をなす(foundational)」ように感じられたからです。他に候補となっていた特性はどれも、CUPIDの特性から派生させることができるでしょう。今後の記事では、最終的に残らなかった特性候補たちが、CUPIDなソフトウェアを書いた結果として自然に備わっていく様子を見ていきたいと考えています。 是非、皆さんのCUPIDとの冒険について聞かせてください。すでに、これらの特性を自分たちのコードを評価するのに採用しているチームや、レガシーなコードベースをきれいにするための開発戦略の策定に活用しているチームがあることを聞き及んでいます。そうした方々の経験談や事例をうかがえることを待ち遠しく思います。それまでの間、私自身としては、CUPIDのそれぞれの特性についてひとつずつ深く探求して、まだ何か隠れている要素がないかを見ていこうと思います。
hr.icon
原注
1. プログラマーに限らずソフトウェア開発にたずさわる全員に、この短いエッセイを読むことをおすすめします。奥深く美しい文章です。
2. 1970年代に、人類学者でキリスト教の宣教学者(宣教師の観察者)のPaul G. Hiebertは「有界集合」と「中心集合」という数学の概念を用いて、「誰が内で誰が外か」というルールで自らを定義する「境界型」コミュニティと、自らを中核をなす価値のまとまりによって定義する「中心型」コミュニティとを対比させました。中心型コミュニティでは人によって中核的価値からの距離に遠近はありますが、そこに「外部」は存在しません。
3. 「単一責任」の定義は「変更する理由は、ひとつだけであるべきである」というものであり、たとえば、UIとビジネスロジックは分離されるべきという考え方です。この制約はいとも簡単に論駁できます。コードの変更は1行だけであっても、その理由はセキュリティ、コンプライアンス、依存元または依存先の影響、運用の観点によるものかもしれません。それだけでなく、私はこの制約を「早まった分離」によって悪影響をおよぼすことの多い、恣意的なものと見ています。
4. Unixオペレーティングシステムの設計における素晴しいシンプルさについてもっと言うと「すべてはファイルであり、すべてはテキストか非テキストである」ことです。プログラム全体を「テキスト変換処理の連なり」として構築できるのです。
5. Rubyはここから外れるかもしれません。Rubyには「Rubyの美学」があり、さまざまな人が「慣習に従ったRuby」について書いています。しかし、これらはあくまで個々人が自身のプログラミングスタイルの好みを共有しているだけであって、コミュニティに内在しているというわけではありせん。 7. 「まっさらな(pristine)」プロジェクトでフレームワークはどれぐらいスキャホールドやボイラープレートを生成して開発者に押しつけるべきかについては、まったく異なる議論もありますが、それはこの記事の範囲外です。
hr.icon
訳注
hr.icon
翻訳について
2022-12-03: 公開
サムネ用画像(Stable Diffusionで雑に生成)
https://gyazo.com/caf9d760178b79ee6791e506e2122e6b